package com.zyl.file.platform;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.HttpMethod;
import com.amazonaws.event.ProgressEvent;
import com.amazonaws.event.ProgressEventType;
import com.amazonaws.event.ProgressListener;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.AmazonS3Exception;
import com.amazonaws.services.s3.model.CompleteMultipartUploadRequest;
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest;
import com.amazonaws.services.s3.model.GetObjectMetadataRequest;
import com.amazonaws.services.s3.model.InitiateMultipartUploadRequest;
import com.amazonaws.services.s3.model.InitiateMultipartUploadResult;
import com.amazonaws.services.s3.model.ListPartsRequest;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PartETag;
import com.amazonaws.services.s3.model.PartListing;
import com.amazonaws.services.s3.model.PartSummary;
import com.amazonaws.services.s3.model.S3Object;
import com.amazonaws.services.s3.model.UploadPartRequest;
import com.amazonaws.services.s3.model.UploadPartResult;
import com.zyl.file.constant.StateConstants;
import com.zyl.file.dto.FilePartInfoDTO;
import com.zyl.file.dto.ProgressMessageDTO;
import com.zyl.file.dto.UploadDTO;
import com.zyl.file.exception.MyException;
import com.zyl.file.po.StEquipment;
import com.zyl.file.po.StFile;
import com.zyl.file.result.ResultCode;
import com.zyl.file.util.FileEncryptUtils;
import com.zyl.file.util.JsonUtils;
import com.zyl.file.vo.UploadSplitVO;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;

import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;

/**
 * @author zyl
 * @Description
 * @since 2024/3/19 10:02
 */
@Slf4j
@Getter
@Setter
@NoArgsConstructor
public class AmazonS3FileStorage implements FileStorage {
    private String platform;
    private String platformType;
    private String bucketName;
    private String domain;
    private String basePath;
    private String defaultAcl;
    private int multipartThreshold;
    private int multipartPartSize;
    private FileStorageClientFactory<AmazonS3> clientFactory;

    public static final Long PRE_SIGN_URL_EXPIRE = 60 * 1000L* 40;

    public AmazonS3FileStorage(
            StEquipment config, FileStorageClientFactory<AmazonS3> clientFactory) {
        platform = String.valueOf(config.getId());
        bucketName = config.getBucket();
        domain = config.getStorageAddress();
        basePath = config.getBasePath();
        platformType = String.valueOf(config.getStorageType());
        this.clientFactory = clientFactory;
        multipartThreshold = 5 * 1024 * 1024;
        multipartPartSize = 5 * 1024 * 1024;
    }

    public AmazonS3 getClient() {
        return clientFactory.getClient();
    }

    @Override
    public StFile getByIdentifier(String identifier, Long id) {
        return null;
    }

    @Override
    public void initTask(StFile stFile) {
        AmazonS3 amazonS3 = getClient();
        //首先获取当前时间作为文件上传到云存储服务的文件夹名称，同时获取云存储服务的bucket名称以及上传的文件名。
        String key = stFile.getObjectKey();
        //根据上传的文件名获取文件后缀名，并使用工具类StrUtil和DateUtil生成唯一的文件名。
        //获取上传文件的Content-Type，并创建一个ObjectMetadata对象来保存Content-Type信息。
        String contentType = MediaTypeFactory.getMediaType(key).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(contentType);
        //使用Amazon S3 SDK的InitiateMultipartUploadRequest方法初始化一个上传请求，并传入bucket名称、文件名和ObjectMetadata对象。
        InitiateMultipartUploadResult initiateMultipartUploadResult = null;
        try {
            initiateMultipartUploadResult = amazonS3.initiateMultipartUpload(new InitiateMultipartUploadRequest(bucketName, key).withObjectMetadata(objectMetadata));
        } catch (AmazonServiceException e) {
            log.error("创建上传任务失败",e);
            throw new RuntimeException(e.getMessage(), e);
        }
        String uploadId = initiateMultipartUploadResult.getUploadId();
        stFile.setUploadId(uploadId);
        stFile.setBucketName(bucketName);
    }

    @Override
    public String getPath(String  key) {
        return  StrUtil.format("{}/{}/{}", domain, bucketName, key);
    }

    @Override
    public List<PartSummary> getTaskInfo(StFile stFile) {
        AmazonS3 amazonS3 = getClient();
        boolean doesObjectExist = amazonS3.doesObjectExist(stFile.getBucketName(), stFile.getObjectKey());
        if (!doesObjectExist){
            ListPartsRequest listPartsRequest = new ListPartsRequest(stFile.getBucketName(), stFile.getObjectKey(), stFile.getUploadId());
            PartListing partListing = amazonS3.listParts(listPartsRequest);
            return partListing.getParts();
        }
        return null;
    }

    @Override
    public String genPreSignUploadUrl(StFile stFile) {
        Map<String, String> params = new HashMap<>(6);
        params.put("partNumber", String.valueOf(stFile.getConfigId()));
        params.put("uploadId", stFile.getUploadId());

        AmazonS3 amazonS3 = getClient();
        Date currentDate = new Date();
        Date expireDate = DateUtil.offsetMillisecond(currentDate, PRE_SIGN_URL_EXPIRE.intValue());
        GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, stFile.getObjectKey())
                .withExpiration(expireDate).withMethod(HttpMethod.PUT);
        if (params != null) {
            params.forEach(request::addRequestParameter);
        }
        URL url = amazonS3.generatePresignedUrl(request);
        return url.toString();
    }

    @Override
    public FilePartInfoDTO uploadSplit(UploadSplitVO uploadSplitVO) {
        AmazonS3 amazonS3 = getClient();
        UploadPartRequest part = new UploadPartRequest();
        FilePartInfoDTO filePartInfoDTO = new FilePartInfoDTO();

        try {
            InputStream inputStream = uploadSplitVO.getInputStream();
            //如果加密，只加密第一个分片
            if (StateConstants.IS_ENCRYPT.equals(uploadSplitVO.getIsEncrypt()) && uploadSplitVO.getPartNumber()==1){
                //调用加密方法
                inputStream = FileEncryptUtils.encryptStream(inputStream, uploadSplitVO.getEncryptKey(), uploadSplitVO.getEncryptIndex());
                uploadSplitVO.setPartSize((long) inputStream.available());
            }
            // Amazon S3 比较特殊，上传分片必须传入分片大小，这里强制获取，可能会占用大量内存
            if (ObjectUtil.isEmpty(uploadSplitVO.getPartSize())){
                uploadSplitVO.setPartSize((long) inputStream.available());
            }
            part.setBucketName(bucketName);
            part.setKey(uploadSplitVO.getKey());
            part.setUploadId(uploadSplitVO.getUploadId());
            part.setInputStream(inputStream);
            part.setPartSize(uploadSplitVO.getPartSize());
            part.setPartNumber(uploadSplitVO.getPartNumber());
            String eTag = amazonS3.uploadPart(part).getPartETag().getETag();
            filePartInfoDTO.setPartSize(uploadSplitVO.getPartSize())
                    .setPartNumber(uploadSplitVO.getPartNumber())
                    .setCreateTime(new Date())
                    .setETag(eTag);
        }catch (Exception e){
            log.error("上传分片文件失败");
            throw new MyException(ResultCode.SYSTEM_BUSY_ERROR, e.toString());
        }
        return filePartInfoDTO;
    }

    @Override
    public void merge(StFile stFile) {
        AmazonS3 amazonS3 = getClient();
        //判断文件是否存在
        boolean doesObjectExist = amazonS3.doesObjectExist(stFile.getBucketName(), stFile.getObjectKey());
        if (!doesObjectExist) {
            ListPartsRequest listPartsRequest = new ListPartsRequest(stFile.getBucketName(), stFile.getObjectKey(), stFile.getUploadId());
            PartListing partListing = amazonS3.listParts(listPartsRequest);
            List<PartSummary> parts = partListing.getParts();
            if (!stFile.getChunkNum().equals(parts.size())) {
                // 已上传分块数量与记录中的数量不对应，不能合并分块
                throw new RuntimeException("分片缺失，请重新上传");
            }
            //合并
            CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest()
                    .withUploadId(stFile.getUploadId())
                    .withKey(stFile.getObjectKey())
                    .withBucketName(stFile.getBucketName())
                    .withPartETags(parts.stream().map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag())).collect(Collectors.toList()));
            amazonS3.completeMultipartUpload(completeMultipartUploadRequest);
        }
    }

    @Override
    public StFile upload(UploadDTO uploadDTO) {
        AmazonS3 amazonS3 = getClient();
        String key = uploadDTO.getKey();
        long fileSize = uploadDTO.getFileSize();
        InputStream inputStream = uploadDTO.getInputStream();
        Long chunkSize = uploadDTO.getChunkSize();
        StFile stFile = new StFile().setBucketName(bucketName)
                .setFilePath(bucketName + "/" + key)
                .setUrl(StrUtil.format("{}/{}/{}", domain, bucketName, key));
        if (multipartThreshold > fileSize){
            //不分片上传
            ObjectMetadata objectMetadata = new ObjectMetadata();
            String contentType = MediaTypeFactory.getMediaType(key).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
            objectMetadata.setContentType(contentType);
            //没有指定内容长度会导致Amazon S3客户端将所有数据缓冲到内存中，而不是以流的方式逐段上传
            objectMetadata.setContentLength(fileSize);
            amazonS3.putObject(bucketName, key, inputStream, objectMetadata);
        }else {
            //初始化一个分块上传，获取分块上传ID，桶名 + 对像名 + 分块上传ID 唯一确定一个分块上传
            InitiateMultipartUploadRequest initRequest = new InitiateMultipartUploadRequest(bucketName, key);
            //您还可以在初始化分块上传时，设置文件的Content-Type
            ObjectMetadata objectMetadata = new ObjectMetadata();
            String contentType = MediaTypeFactory.getMediaType(key).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
            objectMetadata.setContentType(contentType);
            initRequest.setObjectMetadata(objectMetadata);
            //创建上传任务 生成uploadId
            InitiateMultipartUploadResult initResult = amazonS3.initiateMultipartUpload(initRequest);
            String uploadId = initResult.getUploadId();

            ThreadLocal<List<PartETag>> partTags = new ThreadLocal<>();
            partTags.set(new ArrayList<>());
            ThreadLocal<Long> filePosition = new ThreadLocal<>();
            filePosition.set(0L);
            long contentLength = fileSize;
            long partSize = ObjectUtil.isEmpty(chunkSize)? multipartPartSize :chunkSize;
            int partNumber = 1;
            String eTagMarker = null;
            // 检查是否存在之前上传的分片记录
            List<PartETag> eTagList = getExistingPartTags(bucketName, key, uploadId, amazonS3);
            if (eTagList != null && !eTagList.isEmpty()) {
                partTags.set(new ArrayList<>(eTagList));
                partNumber = eTagList.size() + 1;
                // 设置已上传的分片标识
                eTagMarker = eTagList.get(eTagList.size() - 1).getETag();
                filePosition.set(partSize * eTagList.size());
            }
            for (; filePosition.get() < contentLength; partNumber++) {
                // 检查当前分片是否已上传
                if (isPartUploaded(partTags.get(), partNumber)) {
                    continue; // 跳过已上传的分片
                }

                // 计算当前分片大小
                long remainingSize = contentLength - filePosition.get();
                partSize = Math.min(partSize, remainingSize);

                // 创建上传分片请求
                UploadPartRequest uploadRequest = null;
                uploadRequest = new UploadPartRequest()
                        .withBucketName(bucketName)
                        .withKey(key)
                        .withUploadId(uploadId)
                        .withPartNumber(partNumber)
                        .withInputStream(inputStream)
                        .withPartSize(partSize)
                        .withGeneralProgressListener(progressListener(filePosition.get(), contentLength));

                // 上传分片
                UploadPartResult uploadResult = amazonS3.uploadPart(uploadRequest);
                partTags.get().add(uploadResult.getPartETag());
                filePosition.set(filePosition.get() + partSize);
            }
            // 检查所有分片是否已上传完成
            if (isMultipartUploadComplete(bucketName, key, uploadId, partTags.get(), amazonS3)) {
                // 合并分片
                CompleteMultipartUploadRequest completeRequest = new CompleteMultipartUploadRequest(
                        bucketName, key, uploadId, partTags.get());
                amazonS3.completeMultipartUpload(completeRequest);
                log.info("完成合并分片");
            }
            // 清理ThreadLocal中的数据
            partTags.remove();
            filePosition.remove();
            stFile.setUploadId(uploadId);
        }
        return stFile;
    }

    @Override
    public InputStream getFileStream(String key) {
        AmazonS3 amazonS3 = getClient();
        S3Object amazonS3Object = amazonS3.getObject(bucketName, key);
        return amazonS3Object.getObjectContent();
    }

    @Override
    public Long getFileSize(String key) {
        AmazonS3 amazonS3 = getClient();
        // 获取对象元数据
        ObjectMetadata metadata = amazonS3.getObjectMetadata(new GetObjectMetadataRequest(bucketName, key));
        // 获取对象大小
        long size = metadata.getContentLength();
        return size;
    }

    @Override
    public StFile replaceFile(UploadDTO uploadDTO) {
        //删除源文件
        return upload(uploadDTO);
    }

    @Override
    public boolean delete(String url) {
        AmazonS3 client = getClient();
        try {
            log.info("正在删除");
            client.deleteObject(bucketName, url);
            return true;
        } catch (Exception e) {
            log.error("删除失败",e);
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    @Override
    public void download(StFile stFile, Consumer<InputStream> consumer) {

    }

    @Override
    public void close() {
        clientFactory.close();
    }

    /**
     * 获取已上传的分片记录
     * @param bucketName 桶名
     * @param key key
     * @param uploadId 上传id
     * @param amazonS3 AS3对象
     * @return Result
     */
    private List<PartETag> getExistingPartTags(String bucketName, String key, String uploadId, AmazonS3 amazonS3) {
        List<PartETag> eTagList = new ArrayList<>();

        try {
            ListPartsRequest listPartsRequest = new ListPartsRequest(bucketName, key, uploadId);
            PartListing partListing;
            do {
                partListing = amazonS3.listParts(listPartsRequest);
                List<PartSummary> parts = partListing.getParts();
                for (PartSummary partSummary : parts) {
                    PartETag partTags = new PartETag(partSummary.getPartNumber(), partSummary.getETag());
                    eTagList.add(partTags);
                }
                listPartsRequest.setPartNumberMarker(partListing.getNextPartNumberMarker());
            } while (partListing.isTruncated());
        } catch (AmazonS3Exception e) {
            log.error("获取已上传的分片记录失败",e);
            throw new RuntimeException(e.getMessage(), e);
        }

        return eTagList;
    }

    /**
     * 检查分片是否已上传
     * @param partTags 分片标识
     * @param partNumber 分片数量
     * @return Result
     */
    private boolean isPartUploaded(List<PartETag> partTags, int partNumber) {
        for (PartETag partTag : partTags) {
            if (partTag.getPartNumber() == partNumber) {
                return true;
            }
        }
        return false;
    }

    /**
     * 监控进度
     * @param uploadedBytes 上传byte
     * @param totalBytes 最大byte
     * @return Result
     */
    private ProgressListener progressListener(long uploadedBytes, long totalBytes) {
        final long[] uploadedByte = {uploadedBytes};
        return new ProgressListener() {
            @Override
            public void progressChanged(ProgressEvent progressEvent) {
                ProgressEventType eventType = progressEvent.getEventType();
                if (eventType == ProgressEventType.REQUEST_BYTE_TRANSFER_EVENT) {
                    // 更新已上传字节数
                    uploadedByte[0] += progressEvent.getBytesTransferred();
                    // 计算上传进度百分比
                    double progressPercentage = (uploadedByte[0] * 100.00) / totalBytes;
                    log.info("已上传字节数:{}", uploadedByte[0]);
                    log.info("已上传:{}", progressPercentage);
                    // 将上传进度信息传递给前端
                    // 推送进度信息给WebSocket连接
//                    messageApi.sendFinishMsgAndId(stFile.getCreateUser(), createProgressMessage(String.format("%.2f", progressPercentage), stFile));
                } else if (eventType == ProgressEventType.TRANSFER_COMPLETED_EVENT) {
                    log.info("上传完成");
                    // 推送进度信息给WebSocket连接
//                    messageApi.sendFinishMsgAndId(stFile.getCreateUser(), String.valueOf(100));
                } else if (eventType == ProgressEventType.TRANSFER_STARTED_EVENT) {
                    log.info("上传开始");
                }
            }
        };
    }

    /**
     * 检查分片上传是否完成
     * @param bucketName 桶名
     * @param key key
     * @param uploadId 上传id
     * @param partTags 分片标识
     * @param amazonS3 AS3对象
     * @return Result
     */
    private boolean isMultipartUploadComplete(String bucketName, String key, String uploadId, List<PartETag> partTags, AmazonS3 amazonS3) {
        try {
            ListPartsRequest listPartsRequest = new ListPartsRequest(bucketName, key, uploadId);
            PartListing partListing;
            do {
                partListing = amazonS3.listParts(listPartsRequest);
                List<PartSummary> parts = partListing.getParts();
                if (parts.size() != partTags.size()) {
                    return false;
                }
                listPartsRequest.setPartNumberMarker(partListing.getNextPartNumberMarker());
            } while (partListing.isTruncated());
        } catch (AmazonS3Exception e) {
            log.error("检查分片上传是否完成失败",e);
            throw new RuntimeException(e.getMessage(),e);
        }

        return true;
    }

    /**
     * 发送WebSocket的信息
     * @param progressPercentage progressPercentage
     * @param stFile stFile
     * @return String
     */
    private String createProgressMessage(String progressPercentage, StFile stFile) {
        ProgressMessageDTO progressMessage = new ProgressMessageDTO();
        progressMessage.setProgress(Integer.valueOf(progressPercentage));
        progressMessage.setMd5(stFile.getSourceFileMd5());
        progressMessage.setNewFileName(stFile.getOriginalFilename());
        progressMessage.setFileId(stFile.getId());
        // 创建进度信息的 JSON 字符串
        String progressMessages = JsonUtils.toJSONString(progressMessage);
        return progressMessages;
    }
}
